import StringIO
import csv
import itertools
import logging
import os
import urllib
import pickle
import httplib2

from google.appengine.api import users
from google.appengine.ext import webapp
from google.appengine.ext.webapp import util
from google.appengine.ext.webapp import template
from google.appengine.api import memcache
from google.appengine.ext import db
from google.appengine.ext.webapp.util import login_required

from django import newforms as forms
#from django import forms
from django.utils import simplejson

from oauth2client.appengine import CredentialsProperty
from oauth2client.appengine import StorageByKeyName
from oauth2client.client import OAuth2WebServerFlow
from apiclient.discovery import build

import gslite
import google_prediction
import models

# TODO: Port to Django 1.2 templates and forms

class BaseHandler(webapp.RequestHandler):
    """Implements basic functionality useful in all the handlers."""
    def render(self, filename, **context):
        path = os.path.join(os.path.dirname(__file__), 'templates', filename)
        self.response.out.write(template.render(path, context))

class MainPage(BaseHandler):
    """Renders the index page."""

    def get(self):
        self.render("index.html")


class SearchPage(BaseHandler):
    """Renders the live search results page for a model and query."""

    def get(self, model_id):
        query = self.request.GET.get('q')
        model = models.PredictionModel.get_by_id(int(model_id))
        if not model:
            self.error(404)
            return
        self.render("search.html", model_id=model_id, query=query, model=model)


class ClassifyPage(BaseHandler):
    """Handles requests to classify a set of tweets according to the model."""

    def post(self, model_id):
        # Fetch the model information
        model = models.PredictionModel.get_by_id(int(model_id))
        # Decode the tweets sent in the body of the request
        tweet = simplejson.loads(self.request.body)
        prediction = model.predict(tweet)
        tweet['category'] = prediction
        logging.info(simplejson.dumps(tweet))
        # Send the (augmented) tweet back to the client
        self.response.headers['Access-Control-Allow-Origin'] = 'chrome-extension://*'
        self.response.headers['Access-Control-Allow-Headers'] = 'true'
        self.response.headers['Content-Type'] = 'application/json'
        self.response.out.write(simplejson.dumps(tweet))


class PredictionModelForm(forms.Form):
    """Form for creating/modifying prediction models."""

    name = forms.CharField()
    public = forms.BooleanField(required=False)
    categories = forms.CharField()
    acl = forms.CharField()


class ModelCreatePage(BaseHandler):
    """Handles creation of new prediction models."""

    def render_form(self, form):
        self.render("create.html", form=form)

    @util.login_required
    def get(self):
        self.render_form(PredictionModelForm())

    def post(self):
        # Return a 403 if the user isn't logged in.
        # We can't use the login_required decorator here because that will send
        # a redirect, discarding form POST data.
        if not users.get_current_user():
            self.error(403)
            return
        form = PredictionModelForm(self.request.POST)
        if form.is_valid():
            prediction_model = models.PredictionModel(
                name=form.clean_data['name'],
                owner=users.get_current_user(),
                public=form.clean_data['public'],
                categories=form.clean_data['categories'].split(' '),
                acl=form.clean_data['acl'].split(' '),
            )
            prediction_model.put()
            self.redirect('/model/%d/train' % prediction_model.key().id())
        else:
            self.render_form(form)


class TrainPage(BaseHandler):
    """Prediction model training page
       GET - render the training page
       POST - training submission; redirect to trained model
    """
    def get(self, model_id):
        model = models.PredictionModel.get_by_id(int(model_id))
        if not model:
            self.error(404)
            return
        self.render("train.html", model=model)

    def post(self, model_id):
        pmodel = models.PredictionModel.get_by_id(int(model_id))
        if not pmodel:
            self.error(404)
            return
        datafile = StringIO.StringIO()
        data = csv.writer(datafile)
        # The submitted form has fields of the form tweet-text-n and
        # tweet-category-n, where n is an incrementing integer. We iterate
        # through each one, adding the training data to the CSV output until
        # we run out of form fields.
        for i in itertools.count():
            text = self.request.POST.get("tweet-text-%d" % i)
            if not text:
                # No value for this field means we've reached the last one.
                break
            category = self.request.POST.get("tweet-category-%d" % i)
            if not category:
                # No value for this field just means the user didn't categorize
                # the tweet - skip to the next one.
                continue
            lang = self.request.POST["tweet-lang-%d" % i]
            data.writerow((category, lang, text.encode('utf-8')))

        # Write the generated CSV to Google Storage
        config = models.ConfigModel.get_config()
        credentials = config.credentials
        if credentials:
            #do not pass projectid for now, it seems to be invalid
            #storage = gslite.GsClient(oauth=credentials.access_token, projectid=config.storage_project_id)
            storage = gslite.GsClient(oauth=credentials.access_token)
            datafile.seek(0)
            storage.put_object(
                config.bucket_name, "model-%d" % pmodel.key().id(), datafile)
            
            # Tell the prediction API to train on the model
            http = httplib2.Http()
            http = credentials.authorize(http)
            service = build("prediction", "v1.2", http=http)
            training = service.training()
            #weird behavior of prediction api reflection, which requires both data and body params with same values
            result = training.insert(data='%s/model-%d' % (config.bucket_name, pmodel.key().id()),
                                     body='{"id": "%s/model-%d"}' % (config.bucket_name, pmodel.key().id())).execute()
            logging.info(result)

            # Update the datastore record to indicate training has begun.
            pmodel.trained = True
            pmodel.put()

            self.redirect('/model/%d' % pmodel.key().id())
        else:
            #todo better error handling
            self.error(404)
            return

class ModelPage(BaseHandler):
    """Displays information about the selected model."""

    def get(self, model_id):
        model = models.PredictionModel.get_by_id(int(model_id))
        if not model:
            self.error(404)
            return

        # Check the training status
        config = models.ConfigModel.get_config()
        credentials = config.credentials
        if credentials:
            http = httplib2.Http()
            http = credentials.authorize(http)
            service = build("prediction", "v1.2", http=http)
            training = service.training()
            result = training.get(data='%s/model-%d' % (config.bucket_name, model.key().id())).execute()
            logging.info(result)
        else:
            result = None
        self.render("model.html", model=model, result=result)


class GetModelHandler(BaseHandler):
    """Dumps out model information in JSON format."""

    def get(self, model_id):
        model = models.PredictionModel.get_by_id(int(model_id))
        if not model:
            self.error(404)
            return
        self.response.headers['Content-Type'] = 'application/json'
        self.response.out.write(simplejson.dumps({
            'name': model.name,
            'categories': model.categories}))

def get_models(user):
    q = models.PredictionModel.all()
    q.filter("owner =", user)
    results = q.fetch(100)
    return results

class GetModelsHandler(BaseHandler):
    """Dumps out all models for current user in JSON format."""

    def get(self):
        if not users.get_current_user():
            self.error(403)
            return
        results = get_models(users.get_current_user())
        
        #self.response.headers.update({ does not exist, need to set headers one by one
        self.response.headers['Content-Type'] = 'application/json'
        self.response.headers['Access-Control-Allow-Origin'] = 'chrome-extension://*'
        self.response.headers['Access-Control-Allow-Headers'] = 'true'
        models = []
        for model in results:
            models.append({
                'id': model.key().id(),
                'name': model.name,
                'categories': model.categories})
        self.response.out.write(simplejson.dumps(models))

class ModelListPage(BaseHandler):
    """Lists all Models the user can modify"""

    @util.login_required
    def get(self):
        results = get_models(users.get_current_user())
        self.render("list.html", models=results)

#todo streamline this and build a batch version from client to server over json
def add_to_model(config, pmodel, record):
    try:
        credentials = config.credentials
        http = httplib2.Http()
        http = credentials.authorize(http)
        service = build("prediction", "v1.2", http=http)
        training = service.training()
        body = {'classLabel': record.category,
                'csvInstance': [record.category, record.language_code, record.text.encode('utf-8')]
                }
        #you need to pass a dictionnary as body arg to this puppy because he json.dumps the argument himself
        # too smart for his own good:-)
        result = training.update(data='%s/model-%d' % (config.bucket_name, pmodel.key().id()),
                                 body=body).execute()
        return True
    except:
        logging.exception("could post tweet %s" %(record.tid))
        return False
        
class PostHandler(BaseHandler):
    """Handles submissions of new training data for a model."""
    # TODO: Actually send new training data to the Prediction API.

    def options(self, model_id):
        self.response.headers['Access-Control-Allow-Origin'] = '*'
        self.response.headers['Access-Control-Allow-Methods'] = 'POST, GET, OPTIONS'
        self.response.headers['Access-Control-Max-Age'] = 1000
        self.response.headers['Access-Control-Allow-Headers'] = '*'
        self.response.headers['Access-Control-Allow-Headers'] = 'true'

    def post(self, model_id):
        if not users.get_current_user():
            self.error(403)
            return

        config = models.ConfigModel.get_config()
        pmodel = models.PredictionModel.get_by_id(int(model_id))
        if not pmodel:
            self.error(404)
            return
        record = models.TrainingRecord(
            parent=pmodel,
            owner=users.get_current_user(),
            tid=int(self.request.POST.get("tweet-id")),
            language_code=self.request.POST.get("tweet-lang"),
            text=self.request.POST.get("tweet-text"),
            href=self.request.POST.get("tweet-href"),
            category=self.request.POST.get("tweet-cat"))
        record.pushed = add_to_model(config, pmodel, record)
        record.put()
        self.response.headers['Access-Control-Allow-Origin'] = 'chrome-extension://*'
        self.response.headers['Access-Control-Allow-Headers'] = 'true'
        self.response.out.write('{"status": 200}')


ROUTES = [
   ('/', MainPage),
   ('/model/(\d+)', ModelPage),
   ('/model/(\d+)/search', SearchPage),
   ('/model/(\d+)/train', TrainPage),
   ('/model/(\d+)/post', PostHandler),
   ('/model/(\d+)/get', GetModelHandler),
   ('/models/get', GetModelsHandler),
   ('/classify/(\d+)', ClassifyPage),
   ('/model/create', ModelCreatePage),
   ('/model/', ModelListPage),
]


application = webapp.WSGIApplication(ROUTES, debug=True)


def main():
    util.run_wsgi_app(application)


if __name__ == "__main__":
    main()
